'''
Created on Apr 9, 2011

@author: posttool

This module handles login, logout, and basic user management, including a session.
It also provides 3 Request handlers that are used by all mapped urls in the application.
Finally, it provides a database Model superclass that is used by all entities that are
owned by users.
'''
import os
import logging
import datetime
import time
import traceback
import urllib


from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext import db
from google.appengine.ext.webapp import template
from django.utils import simplejson

from gaesessions import get_current_session
from google.appengine.ext.db.metadata import Kind
import pickle

LOGIN_HANDLER_URL  = '/login_handler'
LOGOUT_HANDLER_URL = '/logout_handler'

def get_url_mappings():
	return [
		(LOGIN_HANDLER_URL, LoginHandler),
		(LOGOUT_HANDLER_URL, LogoutHandler)
		]


class ImpactException(Exception):
	IMPACT_ERROR=0x0
	def __init__(self, message,code=IMPACT_ERROR):
		Exception.__init__(self, message)
		self.code=code


OPEN_ID_PROVIDERS = (
	('google','google.com/accounts/o8/id'),
	('yahoo','yahoo.com'),
	('myspace','myspace.com'),
	('aol','aol.com'),
	('myopenid','myopenid.com' )
)

SESSION_UCTX_KEY = '_uctx_'

class LoginHandler(webapp.RequestHandler):

	listeners = []

	def get(self):
		google_user = users.get_current_user()
		if not google_user:
			return self.redirect('/?error=login_error') #TODO i18n
		session = get_current_session()
		uctx = None
		if SESSION_UCTX_KEY in session:
			uctx = session[SESSION_UCTX_KEY]

		if uctx is not None and uctx.is_trial_user:
			trial_user 	   		= uctx.profile.parent()
			trial_user.email 	= google_user.email()
			uctx.profile.email 	= google_user.email()
			uctx.profile 		= uctx.profile.save()
			logging.info("SWITCHED OVER USER PROFILE IN TRY BEFORE BUY")
			uctx.user 		   	= uctx.profile
			uctx.is_trial_user 	= False
		else:
			if session.is_active():
				session.terminate()
			uctx = ImpactUserContext()

			q = db.Query(ImpactUser)
			q.filter("email =",google_user.email())
			results = q.fetch(1)
			if len(results) == 0:
				def txn():
					im_user					= ImpactUser.create_impact_user_from_email(google_user.email())
					uctx.profile 			= UserProfile.create_user_profile_by_impact_user(im_user)
				db.run_in_transaction(txn)
			else:
				im_user = results[0]
				q 		= db.Query(UserProfile)
				q.ancestor(im_user)
				results = q.fetch(1)
				if len(results) != 1:
					raise ImpactException("Couldnt find user profile for impact user %s",im_user.email)
				uctx.profile = results[0]


			uctx.id 				= session.sid
			uctx.user 				= uctx.profile
			uctx.profile.last_login = datetime.datetime.utcnow()
			uctx.profile.save()
			uctx.logged_in 			= True

		session[SESSION_UCTX_KEY] = uctx
		self.redirect(self.request.get('r'))




	@staticmethod
	def add_new_user_listener(callback):
		LoginHandler.listeners.append(callback)

	##TODO move this to a superclass or utility
	##def add_event_listener(type,callback)
	##def remove_event_listeners(type)
	##def remove_event_listener(type,callback)




class LogoutHandler(webapp.RequestHandler):

	def get(self):
		session = get_current_session()
		if session.has_key(SESSION_UCTX_KEY):
			uctx = session[SESSION_UCTX_KEY]
			if uctx.profile.count is None:
				uctx.profile.count = 0

			uctx.profile.count += 1
			uctx.profile.save()
			session.terminate()
			self.redirect(self.request.get('r'))
		else:
			self.redirect('/?error=logout_error') #TODO i18n


class ImpactUserContext():
	def __init__(self):
		self.id 			= None
		self.user 			= None
		self.profile 		= None
		self.logged_in 		= False
		self.is_trial_user 	= False
		self._props		= {}

	def has_prop(self,name):
		return self._props.has_key(name)

	def get_prop(self,name):
		return self._props[name]

	def set_prop(self,name,value):
		self._props[name] = value
		session = get_current_session()
		session[SESSION_UCTX_KEY] = self




class ImpactBaseHandler(webapp.RequestHandler):

	def get(self,args=None):
		self.handle_request(args)

	def post(self,args=None):
		self.handle_request(args)

	def handle_request(self,args=None):
		session = get_current_session()
		if session.has_key(SESSION_UCTX_KEY):
			uctx = session[SESSION_UCTX_KEY]
		else:
			uctx = ImpactUserContext()
			uctx.id = session.sid

		if args != None:
			args = urllib.unquote(args)
		self.process(uctx,args)

	def process(self,uctx,args=None):
		"""abstract method"""
		pass

	def get_input_dict(self):
		params = self.request.arguments();
		inputs = dict();
		for k in params:
			inputs[k] = urllib.unquote(self.request.get(k));
		return inputs;

	def get_login_urls(self):
		logins = []
		for name,url in OPEN_ID_PROVIDERS:
			dest_url = LOGIN_HANDLER_URL + '?r=' + self.request.path
			login_url = users.create_login_url(dest_url=dest_url,federated_identity=url)
			logins.append({'url':login_url, 'title':name})
		return logins

	def get_logout_url(self):
		return users.create_logout_url(LOGOUT_HANDLER_URL+ '?r=' + self.request.path)


class TemplateNotFoundException(Exception):
	pass

class TemplateHandler(ImpactBaseHandler):

	def process(self,uctx,args=None):

		#setup pagedata
		response_data = dict();
		pagedata = dict();
		response_data['date']  	 	= datetime.datetime.utcnow()
		response_data['scheme']  	= self.request.scheme
		response_data['host']  	 	= self.request.host
		response_data['data']	 	= pagedata;

		req_path = self.request.path[1:] or 'index.html';
		response_data['path']  		= req_path

		if not uctx.logged_in or uctx.is_trial_user:
			response_data['logins'] = self.get_login_urls()
		else:
			response_data['logout'] = self.get_logout_url()
		response_data['logged_in'] 	= uctx.logged_in
		response_data['uctx'] 		= uctx

		self.prepare_page(uctx,self.get_input_dict(),pagedata);

		self.response.headers['Content-Type'] = "text/html; charset=utf-8"
		try:
			self.response.out.write(self._render_django_template(self.template,response_data))
		except TemplateNotFoundException:
			self.response.set_status(404)
			self.response.out.write(self._render_django_template("404.html", response_data))
		except Exception:
			self.response.set_status(500)
			self.response.out.write(self._render_django_template("500.html", response_data))
			traceback.print_exc()

	def prepare_page(self,uctx,inputs,pagedata):
		"""abstract"""
		pass

	def _render_django_template(self,path,data):
		logging.info("about to render template "+path+" with data "+str(data))
		if os.path.isfile(path):
			return template.render(path, data)
		raise TemplateNotFoundException,path;



class ServiceHandler(ImpactBaseHandler):

	def process(self,uctx,path):
		args = path.split('/')
		service_name = args.pop(0)
		found = False
		for p in self.__class__.__dict__:
			if not p.startswith('_'):
				if p == service_name:
					found = True
					break
		if not found:
			self.response.out.write('{ "error": 404, "path": "%s" }' % self.request.path)
			return
		try:
			handler = getattr(self, service_name)
		except Exception, exc:
			self.handle_exc(exc)
			return
		try:
			ret_val = handler(uctx, args, self.get_input_dict())
		except Exception, exc:
			self.handle_exc(exc)
			return
		if ret_val == None:
			self.response.out.write("{}")
		elif isinstance(ret_val,basestring):
			#if it is not a dict pass it through as a string. lets us return html from api calls
			#probably a better way to do this
			self.response.out.write(ret_val)
		else:
			self.response.out.write(simplejson.dumps(ret_val))

	def handle_exc(self,exc):
			self.response.set_status(500)
			self.response.out.write('{ "error": "%s", "message": "%s" }' % (exc.__class__.__name__, exc))
			traceback.print_exc()



## BASIC GUARD DECORATOR

class PermissionException(Exception):
	pass


def guard(role):
	def _wrapper(f):
		def _inner(*args, **kwargs):
			#SELF = args[0]
			uctx = args[1]
			if uctx.logged_in:  # and role
				return f(*args, **kwargs)
			else:
				raise PermissionException('no permission!')
		return _inner
	return _wrapper




class ImpactUser(db.Model):#keyname is email
	email 		= db.StringProperty()

	def __eq__(self, other):
		if isinstance(other, ImpactUser):
			return self.email == other.email
		return NotImplemented

	def __ne__(self, other):
		result = self.__eq__(other)
		if result is NotImplemented:
			return result
		return not result


	@staticmethod
	def create_impact_user_from_email(email):
		iu = ImpactUser(email=email)
		iu.put()
		return iu

#UserProfile doesn't inherit from ImpactModel
#because ImpactModel uses UserProfile as its 'user'
#this lets us implement proper hierarchies up to UserProfile
#for impact objects as well as making try before you buy
#manageable
class UserProfile(db.Model):
	created 	= db.DateTimeProperty(auto_now_add=True)
	modified 	= db.DateTimeProperty()
	email 		= db.StringProperty()
	first_name 	= db.StringProperty()
	last_name 	= db.StringProperty()
	bio 		= db.StringProperty(multiline=True)
	last_login 	= db.DateTimeProperty()
	count 		= db.IntegerProperty(default=0)

	def save(self):
		self.modified 	= datetime.datetime.utcnow()
		self.put()
		return self

	def __eq__(self, other):
		if isinstance(other, UserProfile):
			return self.email == other.email
		return NotImplemented

	def __ne__(self, other):
		result = self.__eq__(other)
		if result is NotImplemented:
			return result
		return not result

	def to_dict(self):
		return ImpactModel.to_dict(self)

	@staticmethod
	def create_user_profile_by_impact_user(im_user):
		logging.info("SAVING NEW USER PROFILE....USER IS "+str(im_user.email) )
		profile = UserProfile(count=0, email=im_user.email,parent=im_user)
		profile.put()
		for listener in LoginHandler.listeners:
			listener(profile)
		return profile

## ENTITY SUPERCLASS

IMPACT_DATA_SIMPLE_TYPES = (int, long, float, bool,basestring)
class ImpactModel(db.Model):
	user 		= db.ReferenceProperty(UserProfile)
	modified 	= db.DateTimeProperty()
	created 	= db.DateTimeProperty(auto_now_add=True)

	def save(self,user=None):
		if user != None:
			self.user 		= user
		self.modified 	= datetime.datetime.utcnow()
		self.put()
		return self

	def expand(self,prop):
		list = getattr(self,prop)
		el = db.get(list)
		for i in range(len(list)):
			list[i] = el[i]

	def to_dict(self):
		try:
			MASK_FIELDS = self.__class__.mask
		except AttributeError:
			MASK_FIELDS = ()

		output = {'key':str(self.key())}
		if self.key().name() != None:
			output['key_name'] = str(self.key().name())
		for key, prop in self.properties().iteritems():
			if key in MASK_FIELDS:
				continue
			value = getattr(self, key)
			if value is None or isinstance(value, IMPACT_DATA_SIMPLE_TYPES):
				output[key] = value
			elif isinstance(value, dict):#might want to think about this. pickled properties with references as values are handled like this for now
				for k,v in value.items():
					if isinstance(v,db.Model):
						value[k] = v.to_dict()
				output[key] = value
			elif isinstance(value, list):#might want to think about this. pickled properties with references as values are handled like this for now
				value = [str(v) if isinstance(v,db.Key) else v for v in value]
				value = [v.to_dict() if isinstance(v,db.Model) else v for v in value]
				output[key] = value
			elif isinstance(value, datetime.date):
				# Convert date/datetime to ms-since-epoch ("new Date()").
				ms = time.mktime(value.utctimetuple()) * 1000
				ms += getattr(value, 'microseconds', 0) / 1000
				output[key] = int(ms)
			elif isinstance(value, ImpactUser):
				pass
			elif isinstance(value, UserProfile):
				pass
			elif isinstance(value, db.GeoPt):
				output[key] = {'lat': value.lat, 'lon': value.lon}
			elif isinstance(value, db.Model):
				output[key] = value.to_dict()
			else:
				raise ValueError('cannot encode ' + repr(prop))

		return output

# CUSTOM db.Properties

class PickleProperty(db.Property):
	data_type = db.Blob
	def get_value_for_datastore(self, model_instance):
		value = self.__get__(model_instance, model_instance.__class__)
		if value is not None:
			return db.Blob(pickle.dumps(value))
	def make_value_from_datastore(self, value):
		if value is not None:
			return pickle.loads(str(value))

## SYSTEM ENTITIES



## UTIL
def delete_all_data():
	q = Kind.all()
	for kind in q:
		try:
			while True:
				q = db.GqlQuery("SELECT __key__ FROM "+kind.kind_name)
				assert q.count()
				logging.info("DELETEING "+kind.kind_name)
				db.delete(q.fetch(200))
				time.sleep(0.25)
		except Exception:
			pass


def delete_data_for_user(user):
	q = Kind.all()
	for kind in q.fetch(100): #hope the app doesnt declare more that 100 entities
		q2 = db.GqlQuery('SELECT __key__ FROM '+kind.kind_name+' WHERE user = :1',user)
		for ud in q2.fetch(1000): #TODO make this work for more than 1000
			db.delete(ud)

class util:
	@staticmethod
	def query_to_dict_list(query,limit=None,offset=0):
		ret = []
		if limit:
			query = query.fetch(limit=limit,offset=offset);
		if query is not None:
			for item in query:
				ret.append(item.to_dict())
		return ret;

	@staticmethod
	def query_to_json(query,limit=None,offset=0):
		ret = []
		if limit:
			query = query.fetch(limit=limit,offset=offset);
		if query is not None:
			for item in query:
				ret.append(item.to_dict())
		return simplejson.dumps(ret);



	@staticmethod
	def entity_to_json(entity):
		return simplejson.dumps(entity.to_dict())

	@staticmethod
	def in_production():
		return not (os.environ.get('SERVER_SOFTWARE','').startswith('Dev'))
	@staticmethod
	def in_development():
		return not util.in_production()

	@staticmethod
	def render_django_template(path,data):
		#logging.info("about to render template "+path+" with data "+str(data))
		if os.path.isfile(path):
			return template.render(path, data)
		raise TemplateNotFoundException,path;

	@staticmethod
	def date_floor(date,unit,resolution):
		if unit == 'DAY':
			#every resolution minute
			date -=  datetime.timedelta(days = date.timetuple().tm_yday % resolution,
										hours 	  	= date.hour,
										minutes	  	= date.minute,
		                             	seconds	  	= date.second,
		                              	microseconds = date.microsecond)
		elif unit == 'HOUR':
			#every resolution hour
			date -=  datetime.timedelta(hours 	  	= date.hour % resolution,
										minutes	  	= date.minute,
			                         	seconds	  	= date.second,
			                          	microseconds = date.microsecond)
		elif unit == 'MINUTE':
			#every resolution minute
			date -= datetime.timedelta(minutes=date.minute % resolution,
	                             	  seconds=date.second,
	                                  microseconds=date.microsecond)
		return date,util.key_for_floored_date(date)

	@staticmethod
	def key_for_floored_date(d):
		return time.strftime("%Y-%m-%d-%H:%M",d.timetuple())

